Skip to content

StaticRecomp: deliver pending external interrupts at EE-enabled dispatch boundaries - #18

Closed
dougchansan wants to merge 14 commits into
ExpansionPak:mainfrom
dougchansan:fix/extint-ee-delivery
Closed

StaticRecomp: deliver pending external interrupts at EE-enabled dispatch boundaries#18
dougchansan wants to merge 14 commits into
ExpansionPak:mainfrom
dougchansan:fix/extint-ee-delivery

Conversation

@dougchansan

@dougchansan dougchansan commented Aug 23, 2026

Copy link
Copy Markdown

Summary

Runtime half of the Pokemon Colosseum interrupt-starvation fix (backend half now upstream in DolRecomp 1bec355 as emitStateWrite's EE exit; investigation in ExpansionPak/DolRecomp#20). Without this, the mtmsr side exit creates a dispatch boundary but a pending external interrupt still waits for the next timing slice -- and native bursts surface almost exclusively inside interrupt-disabled OS windows, so Colosseum's retrace wait starves and renders one frame per 3-second timeout (0.25 fps).

Delivery semantics, final form: the burst loop delivers a pending external interrupt only at boundaries whose preceding guest instruction is mtmsr -- the same delivery points the block-ending JITs use (they end the block at mtmsr and check there). Two earlier iterations are worth recording:

  • Delivering at every EE-enabled boundary fixes Colosseum but preempts handlers that run their callback with interrupts enabled (the AX audio frame callback) mid-work, and could stall any backend probabilistically at scene transitions -- measured as the C backend crawling in one of two uncapped runs.
  • The final mtmsr scoping keeps the fix (Colosseum capped: 0.25 -> 25-29 fps vs 30 for C; cold boot passes the THP intros) and removes the instability (C: 3/3 clean uncapped passes at 53.6-56.8 fps; AOT arm clean; Mario Kart control 51 fps, no regression).

A second commit adds two lockstep options used in the diagnosis: STATICRECOMP_LOCKSTEP_FILTER=<substr> (report only divergences containing the substring) and STATICRECOMP_LOCKSTEP_NODEDUP=1 (re-check entry PCs; the checked-PC dedup otherwise caps output at one report per diverging entry).

siahisaforker and others added 14 commits July 12, 2026 21:55
Refactor target_include_directories for MSVC and non-MSVC.
The burst loop only broke for hook-raised synchronous exceptions;
external interrupts waited for the next timing slice. The interpreter
effectively offers a delivery point at every block boundary between an
interrupt enable and the following disable, but native bursts surface at
budget-guard exits, which sit at call sites and land almost entirely
inside interrupt-disabled OS windows. A guest waiting in a yield-spin
loop then starves: Pokemon Colosseum held a pending VI interrupt with
MSR[EE]=0 at 30% of delivery checks and rendered one frame per 3-second
timeout.

Break the burst and deliver as soon as a boundary shows a pending
external interrupt with MSR[EE] set. Paired with the DolRecomp change
that exits to the dispatcher when mtmsr enables EE, Colosseum goes from
0.25 to 28 fps against 30 for the C backend, and cold-boots past its THP
intros. Mario Kart Double Dash is unaffected (48.7 -> 51.0).
STATICRECOMP_LOCKSTEP_FILTER=<substr> reports only divergences whose
diff contains the substring, and STATICRECOMP_LOCKSTEP_NODEDUP=1
re-checks entry PCs instead of once each. By default the checked-PC set
capped output at one report per diverging entry, which read as "74
divergences" when it meant 74 distinct PCs; filtering to msr and
disabling the dedup is what exposed the EE starvation pattern on
Colosseum.
Delivering at every EE-enabled dispatch boundary preempts handlers that
run their callback with interrupts enabled (the AX audio frame callback)
in the middle of their work, and re-entering them at each boundary can
consume the guest instead of letting the callback finish. The
block-ending JITs deliver only where the guest executed mtmsr; match
that by checking the instruction before the boundary PC.

On Colosseum this removes a probabilistic stall that could catch every
backend at scene transitions: the C backend went from one crawl in two
uncapped runs to three clean passes at 53.6-56.8 fps, and the AOT arm
stays clean. The mtmsr delivery point keeps the retrace-wait fix intact
(the capped scene still renders at 25-29 fps against 30 for C).
@dougchansan

Copy link
Copy Markdown
Author

Withdrawing this. The evidence it was built on does not hold up.

The Colosseum symptom this PR fixed was an artifact of a modified DOL. The extracted game we were benchmarking, Pokemon-Colosseum-USA, turned out to carry a widescreen mod baked into main.dol: 0x800A3930 had fmr f29,f2 replaced with a branch into hand-injected FP code at 0x80005300 (previously zero padding). A clean extraction of the same title has FFA01090 / 00000000 at those addresses.

Re-measured against a clean DOL (Colo-Fresh), with fresh savestates captured on that DOL, Colosseum runs at full speed with no delivery fix present at all -- this branch reverted to the commit before the first delivery change:

savestate fps vps speed
title 59.95 59.95 0.999
early 59.94 59.92 0.996
mid 60.24 60.25 1.010

Locked to 60 fps, frames advancing, no starvation and no hang. The 0.25 fps retrace-timeout behaviour, the blocked_ee ratios, and the interrupt counters quoted in this PR and in ExpansionPak/DolRecomp#20 were all measured on the modded binary and should not be relied on.

Two things worth keeping out of this:

  1. The lockstep options in the second commit are independently useful and unaffected by any of the above -- STATICRECOMP_LOCKSTEP_FILTER=<substr> and STATICRECOMP_LOCKSTEP_NODEDUP=1. If they are wanted I will open them as their own PR with no performance claims attached.
  2. There is still something real here, at low priority. On the modded DOL the C backend copes with the injected code at 0x80005300 and the LLVM backend does not. That is a genuine backend difference worth its own issue, but it is about handling code injected into former padding, not about interrupt delivery, and it does not affect the shipping game.

Apologies for the noise. The lesson we are adopting: benchmark only from cleanly extracted, unmodified DOLs, and verify main.dol against the untouched extraction before trusting any number from it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants